using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using System.Globalization;

namespace Serialization
{
    class ObjectWriter
    {
        private ObjectIDGenerator _generator = new ObjectIDGenerator();
        private ISurrogateSelector _selector;

        public ObjectWriter()
        {
        }

        public ObjectWriter(ISurrogateSelector selector)
        {
            _selector = selector;
        }

        public void Write(object obj, Stream s)
        {
            BinaryWriter writer = new BinaryWriter(s);
            Write(obj, writer);
        }

        private void Write(object obj, BinaryWriter writer)
        {
            TypeInfo typeInfo = TypeUtil.DetermineObjectType(obj);
            writer.Write((byte)typeInfo);
            if(TypeUtil.IsObjectNull(typeInfo))
            {
                return;
            }
            
            Type objType = obj.GetType();
            int typeCode = TypeUtil.GetTypeCode(objType);

            if (TypeUtil.IsArray(typeInfo))
            {
                Type elemType = objType.GetElementType();
                if (TypeUtil.IsPrimitiveType(elemType))
                {
                    writer.Write(TypeUtil.GetTypeCode(elemType));
                }
                else
                {
                    WriteTypeName(writer, typeInfo, elemType);
                }
                
                Array a = (Array)obj;
                writer.Write(a.Rank);
                for (int i = 0; i < a.Rank; i++)
                {
                    writer.Write(a.GetLength(i));
                }
                int[] indices = new int[a.Rank];
                WriteArrayElements(writer, elemType, a, 0, indices);
            }
            else if (TypeUtil.IsPrimitiveType(typeInfo))
            {
                writer.Write((int)typeCode);
                WritePrimitiveType(writer, typeCode, obj);
            }
            else
            {
                if (TypeUtil.IsReferenceType(typeInfo))
                {
                    bool firstTime = false;
                    long objId = obj == null ? 0 : _generator.GetId(obj, out firstTime);
                    writer.Write(objId);
                    if (!firstTime || objId == 0)
                    {
                        return;
                    }
                }
                WriteTypeName(writer, typeInfo, objType);
                ISurrogateSelector selectedSelector = null;
                ISerializationSurrogate surrogate = TypeUtil.FindSurrogate(objType, _selector, out selectedSelector);
                if (surrogate != null)
                {
                    SerializationInfo info = GetSerializationInfo(obj, objType, surrogate);
                    WriteSerializationInfo(writer, info);
                }
                else
                {
                    Stack<Type> typeStack = TypeUtil.GetTypeStack(objType);
                    foreach (Type t in typeStack)
                    {
                        SerializationInfo info = GetSerializationInfo(obj, t, null);
                        WriteSerializationInfo(writer, info);
                    }
                }
                
            }
        }

        private void WriteTypeName(BinaryWriter writer, TypeInfo typeInfo, Type objType)
        {
            string asmName = objType.Module.Assembly.GetName().Name;
            string typeName = objType.FullName + ", " + asmName;
            writer.Write(typeName);
        }

        private void WriteSerializationInfo(BinaryWriter writer, SerializationInfo info)
        {
            writer.Write(info.Count);
            foreach (SerializationMemberInfo smi in info)
            {
                writer.Write(smi.Name);
                Write(smi.Value, writer);
            }
        }
        
        private void WriteArrayElements(BinaryWriter writer, Type elemType, Array a, int dimension, int[] indices)
        {
            int length = a.GetLength(dimension);
            for (int i = 0; i < length; i++)
            {
                indices[dimension] = i;
                if (dimension == a.Rank - 1)
                {
                    object value = a.GetValue(indices);
                    Write(value, writer);
                }
                else
                {
                    WriteArrayElements(writer, elemType, a, dimension + 1, indices);
                }
            }
        }

        private void WritePrimitiveType(BinaryWriter writer, int typeCode, object value)
        {
            switch((TypeCode)typeCode)
            {
                case TypeCode.Byte:
                    writer.Write((byte)value);
                    break;
                case TypeCode.SByte:
                    writer.Write((sbyte)value);
                    break;
                case TypeCode.Char:
                    writer.Write((char)value);
                    break;
                case TypeCode.Boolean:
                    writer.Write((bool)value);
                    break;
                case TypeCode.Int16:
                    writer.Write((short)value);
                    break;
                case TypeCode.UInt16:
                    writer.Write((ushort)value);
                    break;
                case TypeCode.Int32:
                    writer.Write((int)value);
                    break;
                case TypeCode.UInt32:
                    writer.Write((uint)value);
                    break;
                case TypeCode.Int64:
                    writer.Write((long)value);
                    break;
                case TypeCode.UInt64:
                    writer.Write((ulong)value);
                    break;
                case TypeCode.Single:
                    writer.Write((float)value);
                    break;
                case TypeCode.Double:
                    writer.Write((double)value);
                    break;
                case TypeCode.String:
                    writer.Write((string)value);
                    break;
                case TypeCode.Decimal:
                    decimal d = (decimal)value;
                    writer.Write(d.ToString(CultureInfo.InvariantCulture));
                    break;
                case TypeCode.DateTime:
                    writer.Write(((DateTime)value).Ticks);
                    break;
                case (TypeCode)TypeUtil.TypeCodeTimeSpan:
                    writer.Write(((TimeSpan)value).Ticks);
                    break;
                case (TypeCode) TypeUtil.TypeCodeIntPtr:
                    writer.Write(((IntPtr)value).ToInt64());
                    break;
                case (TypeCode) TypeUtil.TypeCodeGuid:
                    writer.Write(((Guid)value).ToByteArray());
                    break;
                default:
                    throw new Exception("Unknown type code " + typeCode);
            }
        }

        //private void WritePrimitiveType(BinaryWriter writer, SerializationMemberInfo smi)
        //{
        //    WritePrimitiveType(writer, smi.Type, smi.Value);        
        //}

        

        private SerializationInfo GetSerializationInfo(object obj, Type objType, ISerializationSurrogate surrogate)
        {
            SerializationInfo info = new SerializationInfo(objType);
            if (surrogate != null)
            {
                surrogate.GetObjectData(obj, info);
                return info;
            }
                
            if (typeof(ISerializable).IsAssignableFrom(objType))
            {
                ISerializable s = (ISerializable)obj;
                s.GetObjectData(info);
            }
            else
            {
                GetObjectData(obj, objType, info);
            }
            return info;
        }

        private void GetObjectData(object obj, Type objType, SerializationInfo info)
        {
            FieldInfo[] fields = objType.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
            foreach (FieldInfo fi in fields)
            {
                if ((fi.Attributes & FieldAttributes.NotSerialized) != 0)
                {
                    continue;
                }
                object value = fi.GetValue(obj);
                info.AddValue(fi.Name, value, fi.FieldType);
            }
        }

    }
}
